refactor(core): extract the product-agnostic foundation into Nordstein.Core - #535
Conversation
…n.Core
A second Nordstein product would otherwise reimplement the parts of this
repository that are not about LLM tracing. This moves the least entangled slice
of that foundation into `core/` as Nordstein.Core, on its way to a separate
private repository published as NuGet packages, and validates the whole
mechanism end to end before a second repository exists to get any of it wrong.
Moved (history preserved via git mv, all under the `core/` prefix so the split
completes with a `git subtree split` rather than a copy):
Proxytrace.Common -> core/Nordstein.Core.Common
Proxytrace.Common.Tests -> core/Nordstein.Core.Common.Tests
Proxytrace.Testing -> core/Nordstein.Core.Testing
Core is its own solution and is deliberately not part of Proxytrace.sln: the
moment it only compiles as part of the product it stops being extractable.
Consuming projects now declare an item instead of a reference:
<NordsteinCoreReference Include="Nordstein.Core.Common" />
Directory.Build.targets expands it into a ProjectReference (source mode, the
default while the sources are present) or a PackageReference (package mode).
The indirection is what keeps a Core change a one-build edit; without it every
cross-boundary change becomes edit/pack/bump/restore/retest, and the
predictable result is that people copy code into the product instead — the
exact failure the extraction exists to prevent.
CI covers both modes. `backend` builds and tests the Core solution standalone
before the product; the new `core-package` job packs Core and rebuilds the
whole product against the resulting .nupkg files, which is what catches a type
that is public in source but missing from the package surface, and a dependency
Core forgot to declare because source mode resolved it through the product's
own graph.
The backend Dockerfiles gain the build-config files in their restore layer:
without Directory.Build.props/.targets present, the reference expansion yields
nothing and restore silently misses the dependency.
Packaging is wired up but nothing is published — the feed and the licence are
decisions, recorded in core/PUBLISHING.md. The packed LICENSE is a placeholder
copy of the product's.
Docs: new docs/code-reuse.md (indexed in CLAUDE.md) covers the mechanism, the
one rule that Core may not reference the product, and what the next slices are
and what blocks them. architecture.md, ci.md, commands.md, testing.md,
security.md, validation.md, code-style.md, releasing.md and the create-domain
and test skills follow the rename.
No user-facing change, so no CHANGELOG entry.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VA7JB61dw77kiymoNoeGqt
The `core-package` CI job failed on every project with
NU1301: The local source '.../Proxytrace.Domain/core/artifacts' doesn't exist
NuGet resolves a relative `RestoreAdditionalProjectSources` against each
*project* directory rather than the repository root, so passing
`core/artifacts` on the command line asks for `Proxytrace.Domain/core/artifacts`
and every project misses.
Worse, the failure is invisible whenever the packages are already in the global
packages folder: NuGet never reaches for the source, so a local run that has
restored them once before passes and only a cold cache — CI — fails. That is how
this shipped.
The source is now added in Directory.Build.props, built from
$(MSBuildThisFileDirectory) so it is absolute and no caller can get the relative
form wrong, and gated on the directory existing so a real package-mode build
against a published feed is unaffected. The CI job, nuget.config and the docs
drop the flag.
Verified with the packages deleted from ~/.nuget/packages first, so the restore
genuinely has to reach the feed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VA7JB61dw77kiymoNoeGqt
|
First CI run turned up two failures. One was mine, one is not.
NuGet resolves a relative The part worth recording: this is invisible whenever the packages are already in The feed is now added in
Reproduced on a clean Local state on this branch with the advisory suppressed: Core 198 passed standalone, product 2,805 passed / 8 skipped / 0 failed, both reference modes building clean. Generated by Claude Code |
|
#534 landed (#536), so the blocker is gone. Merged Everything below now runs without
CI should now get past restore on both Generated by Claude Code |
Dry-running the extraction turned up two things that would have bitten on the day of the split. 1. core/Directory.Build.props imported the parent Directory.Build.props unconditionally. In its own repository there is no file above it, GetPathOfFileAbove returns empty, and `<Import Project="">` fails with MSB4020 before a single project compiles — the extracted repository would not have built at all. The import is now conditional, and the three things the parent supplies that Core actually needs (TreatWarningsAsErrors, IncludeSourceRevisionInInformationalVersion, the NordsteinCoreVersion default) are defaulted locally, so both layouts behave identically. Verified by splitting core/ into a scratch worktree: it builds, its 198 tests pass, and it packs. Nested in Proxytrace it is unchanged (Version 0.1.0-dev, warnings still errors). 2. The docs claimed `git subtree split --prefix=core` would complete the move "rather than a copy that loses the past". It does not. Subtree split filters strictly by path and does not follow renames, and everything under core/ was git mv'd from Proxytrace.Common/ and friends — so it produces exactly one commit. Measured, not assumed. `git filter-repo` with the pre-move paths mapped onto the current ones keeps the real history: 7 commits on this repository, back through the changes that shaped the code. PUBLISHING.md now carries that recipe, the ordering constraint on the renames, and the standalone build/test/pack check to run before pushing the new repository. README.md and docs/code-reuse.md are corrected to match. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VA7JB61dw77kiymoNoeGqt
A spike of the first slice of shared-code extraction, ahead of a second Nordstein product. It moves the least entangled foundation code into
core/as Nordstein.Core and validates the full mechanism — packaging, namespaces, dependency direction, Docker restore layers, CI, and the extraction itself — before a second repository exists to get any of it wrong.Nothing is published. The feed and the licence are open decisions, recorded in
core/PUBLISHING.md.What moved
Proxytrace.Commoncore/Nordstein.Core.CommonProxytrace.Common.Testscore/Nordstein.Core.Common.TestsProxytrace.Testingcore/Nordstein.Core.TestingCore is its own solution and deliberately not part of
Proxytrace.sln. The moment it only compiles as part of the product, it stops being extractable and nobody finds out until the split is attempted.How the product consumes it
Consuming projects declare a
NordsteinCoreReferenceitem rather than a reference — seeProxytrace.Domain.csprojfor a live example, andDirectory.Build.targetsfor the expansion.Directory.Build.targetsturns that item into aProjectReference(source mode — the default while Core's sources are present) or aPackageReferenceat$(NordsteinCoreVersion)(package mode).The indirection is the load-bearing part. Without it, every cross-boundary change becomes edit → pack → bump → restore → retest, and the predictable result is that nobody makes small Core improvements any more — they copy the code into the product instead, which is the exact failure the extraction exists to prevent. Once Core is its own repository,
NordsteinCorePathpoints at a sibling checkout (../Core/) and nothing else changes.What keeps the boundary honest
Staging Core inside this repository is convenient but removes the natural barrier — nothing but review stops a Proxytrace type being referenced in there. Two mechanical guards replace it:
backendbuilds and testscore/Nordstein.Core.slnstandalone, before the product.core-package(new job) packs Core and rebuilds the whole product against the resulting.nupkgfiles. Project references hide two consumer-only failures: a type that is public in source but never made it into the package surface, and a dependency Core forgot to declare because source mode resolved it through the product's own graph. Packages are uploaded as an artifact, not pushed.The extraction was dry-run, and it did not work the first time
The point of a spike is to find this now rather than on the day of the split. Splitting
core/into a scratch worktree turned up two defects, both fixed in 0efa3b0:The extracted repository would not have built at all.
core/Directory.Build.propsimported the parentDirectory.Build.propsunconditionally. Standalone there is no file above it,GetPathOfFileAbovereturns empty, and importing an empty project path fails withMSB4020before a single project compiles. The import is now conditional, and the three things the parent supplies that Core needs are defaulted locally.git subtree splitis the wrong tool and loses the history. It filters strictly by path and does not follow renames — and everything undercore/wasgit mv'd fromProxytrace.Common/and friends. Measured: 1 commit.git filter-repowith the pre-move paths mapped onto the current ones keeps the real history: 7 commits, back through Trace sessions: higher-level live grouping for debugging #371, fix(storage,di): invalidate the entity cache after commit; populate framework plumbing once (#450, #451) #465, Various Bugfixes #484, Bump the nuget-minor-patch group with 9 updates #508 and the background-service hosting fix.core/PUBLISHING.mdnow carries the verified recipe, the ordering constraint on the renames, and the standalone build/test/pack check to run before pushing the new repository.An earlier revision of this description claimed a subtree split would preserve the past. That was wrong; this is the correction.
Other changes worth a reviewer's attention
Proxytrace.Api,Proxytrace.Proxy.Api,deploy/allinone) copyDirectory.Build.props/.targets,nuget.configand Core's project file into the restore layer. Without them the reference expansion yields nothing and restore silently misses the dependency.nuget.configis new: aclearelement plus nuget.org, so a machine-level source cannot supply a package this repository did not intend to restore. The local package-mode feed comes fromDirectory.Build.propsby absolute path — NuGet resolves a relativeRestoreAdditionalProjectSourcesper project directory, which fails only once the packages are absent from the global cache (that is how it passed locally and failed in CI; see the thread).detect-changestreats^core/,Directory.Build.targetsandnuget.configas backend changes.dotnet test Proxytrace.sln. A cross-cutting local run now needs both solutions;docs/testing.mdanddocs/commands.mdsay so.Docs
New
docs/code-reuse.md(indexed inCLAUDE.md) covers the mechanism, the one rule — Core may not reference the product — and the next slices with what blocks each: the domain/storage foundation (assembly-hardwired reflection inDomain.Module/Storage.Module,internalseams,MigrationsAssemblypinning), then licensing, then the cross-cutting subsystems, then the frontend primitives.architecture.md,ci.md,commands.md,testing.md,security.md,validation.md,code-style.md,releasing.mdand thecreate-domain/testskills follow the rename.No user-facing change, so no CHANGELOG entry.
Verification
All 16 CI checks are green on
0efa3b0, includingcore-package(compiles the product against the packed.nupkgfiles) andbackend(both solutions, withPROXYTRACE_REQUIRE_DOCKER_TESTS=true), plus e2e and the all-in-one image boot.Locally, all without
-p:NuGetAudit=falsesince #534 landed:dotnet build core/Nordstein.Core.slndotnet test core/Nordstein.Core.slndotnet build Proxytrace.sln(source mode)dotnet build Proxytrace.sln(package mode, packages cleared from the global cache first)dotnet test Proxytrace.slnperf/projectsFull suite rather than a narrow scope, because the change is cross-cutting by definition.
Open decisions (not in this PR)
core/LICENSEis a placeholder copy of the product's. A licence cannot be recalled from consumers who already restored the package.Nordstein.*on nuget.org, before any public push.🤖 Generated with Claude Code
https://claude.ai/code/session_01VA7JB61dw77kiymoNoeGqt